3. Cpp面向对象编程(OOP)进阶
1 复制控制
关于类复制的复制构造函数和赋值操作符重载等统称为复制控制。
1.1 Copy Constructor (复制构造函数)
在按值传递,以及返回非引用、非指针的返回值时,如果变量是基本数据类型,编译器会直接复制变量的值到临时变量中去。如果变量是类实例,编译器就会通过类的复制构造函数,使用现有对象的成员的数据重新构造一个临时变量:
class MyClass {
public:
MyClass(int aa) : a(aa) {}
MyClass(const MyClass &myclass) : a(myclass.a) { // 复制构造函数
cout << "MyClass的拷贝构造函数被调用!" << endl;
}
private:
int a;
};
MyClass test(MyClass myclass) {
cout << "test函数开始执行!" << endl;
return myclass;
}
int main() {
MyClass myclass1(2);
cout << "显式调用复制构造函数!" << endl; // -> 显式调用复制构造函数!
MyClass myclass2(myclass1); // -> MyClass的拷贝构造函数被调用!
cout << "test函数调用前!" << endl; // -> test函数调用前!
test(myclass2); // -> MyClass的拷贝构造函数被调用!\n test函数开始执行!\n MyClass的拷贝构造函数被调用!
cout << "test函数调用后!" << endl; // test函数调用后!
MyClass myclassArr[3] = { myclass1, myclass1, myclass2 }; // ->MyClass的拷贝构造函数被调用!\n MyClass的拷贝构造函数被调用!\n MyClass的拷贝构造函数被调用!
return 0;
}
注意复制构造函数的定义:
MyClass(const MyClass &myclass) : a(myclass.a) { // 复制构造函数
cout << "MyClass的拷贝构造函数被调用!" << endl;
}
复制构造函数和构造函数的区别主要在于形参列表,这里有两个注意点:
- 被复制的类的形参为
const,因为复制不需要改变原来的对象,同时也是出于数据安全的考虑; - 使用按引用
&传递,设想如果使用按值传递,就会触发复制构造,复制构造再调用复制构造函数,就会无限循环下去。
1.2 合成的复制构造函数
在没有自定义 Copy Constructor 时,编译器会自动合成默认的 Copy Constructor:
class MyClass {
public:
MyClass(int aa, int bb) : a(aa), b(bb) {}
void printValues() {
cout << "a的值为:" << a << endl;
cout << "b的值为:" << b << endl;
}
private:
int a;
int b;
};
MyClass test(MyClass myclass) {
return myclass;
}
int main() {
MyClass myclass1(2, 4);
myclass1.printValues();
cout << "调用test!" << endl;
test(myclass1).printValues(); //让test返回的副本对象调用函数
return 0;
}
--------------------------------------------------------
输出:
a的值为:2
b的值为:4
调用test!
a的值为:2
b的值为:4 // test()在传参和返回时,使用了编译器自动合成的复制构造函数
注意,合成复制构造函数只能实现基本的行为:复制所有成员,如果成员中有类对象,就调用/合成其 Copy Constructor。但成员为指针时,合成复制只会复制指针的值,即地址,而不会复制指针指向的对象:
// 浅复制
class Component {
public:
Component(int v) : val(v) {}
int getVal() { return val; }
private:
int val;
};
class MyClass {
public:
MyClass(Component cp, Component *cpPtr) : comp(cp), compPtr(cpPtr) {}
void print() {
cout << "comp的val值为:" << comp.getVal() << endl;
cout << "compPtr的地址为:" << compPtr << endl;
cout << "compPtr指向对象的val值为:" << compPtr->getVal() << endl;
}
private:
Component comp;
Component *compPtr;
};
MyClass test(MyClass myclass) {
return myclass;
}
int main() {
Component comp1(1);
Component comp2(3);
MyClass myclass1(comp1, &comp2);
myclass1.print();
cout << "调用test!" << endl;
test(myclass1).print();
return 0;
}
---------------------------------------------------------
输出:
comp的val值为:1
compPtr的地址为:0x29a8bffc28
compPtr指向对象的val值为:3
调用test!
comp的val值为:1
compPtr的地址为:0x29a8bffc28
compPtr指向对象的val值为:3
可以发现,在合成复制构造函数调用前后,compPtr 的地址的地址没有变化,也就是指向的是被复制的类的成员,而不是自身所处类实例的成员。如果 MyClass 采用动态分配,那么在调用 myclass1 的析构函数之后,再次调用 test(myclass1) 的析构函数,会出现 compPtr 指向的对象已被释放的情况,这被称为浅复制。要实现深复制,需要借助自定义 Copy Constructor:
// 深复制
class Component {
public:
Component(int v) : val(v) {}
int getVal() { return val; }
private:
int val;
};
class MyClass {
public:
MyClass(Component cp, Component *cpPtr) : comp(cp), compPtr(cpPtr) {}
MyClass(const MyClass &myclass) : comp(myclass.comp) { // 深复制
this->compPtr = new Component(*myclass.compPtr);
// *myclass.compPtr是myclass对象的compPtr指针指向的Component对象comp2.
// 所以new Component(*myclass.compPtr)是分配一个新的Component对象,并用*myclass.compPtr初始化它.
}
~MyClass() {
delete compPtr;
}
void print() {
cout << "comp的val值为:" << comp.getVal() << endl;
cout << "compPtr的地址为:" << compPtr << endl;
cout << "compPtr指向对象的val值为:" << compPtr->getVal() << endl;
}
private:
Component comp;
Component *compPtr;
};
MyClass test(MyClass myclass) {
return myclass;
}
int main() {
Component comp1(1);
Component comp2(3);
MyClass myclass1(comp1, &comp2);
myclass1.print();
cout << "调用test!" << endl;
test(myclass1).print();
return 0;
}
----------------------------------------------------------------------------
输出:
comp的val值为:1
compPtr的地址为:0xa2b3bff8d8
compPtr指向对象的val值为:3
调用test!
comp的val值为:1
compPtr的地址为:0x1d66285dc70
compPtr指向对象的val值为:3
(exited with code=3221226356 in 0.766 seconds)
注意,如果 Has-a 父类 Component 也存在指针成员,那么其也需要自定义 Copy Constructor 以实现深拷贝。
1.3 Overload assignment (重载赋值操作符)
对两个类实例使用赋值操作符 = ,会把右值的所有成员的值复制到左值的对象中去,这也是浅拷贝。
class Component {
public:
Component(int v) : val(v) {}
int getVal() { return val; }
private:
int val;
};
class MyClass {
public:
MyClass(Component cp) : comp(cp) {}
void print() {
cout << "comp的val值为:" << comp.getVal() << endl;
}
private:
Component comp;
};
int main() {
Component comp1(1);
MyClass myclass1(comp1);
myclass1.print();
Component comp2(2);
MyClass myclass2(comp2);
myclass2.print();
cout << "进行赋值!" << endl;
myclass2 = myclass1;
myclass2.print();
return 0;
}
-----------------------------------------------------
输出:
comp的val值为:1
comp的val值为:2
进行赋值!
comp的val值为:1
重载赋值操作符可以实现深拷贝:
class Component {
public:
Component(int v) : val(v) {}
int getVal() { return val; }
private:
int val;
};
class MyClass {
public:
MyClass(int val) {
compPtr = new Component(val);
}
~MyClass() {
delete compPtr;
}
MyClass& operator=(const MyClass &rhs) { // 赋值操作符重载,rhs表示右值(right hand side)
*compPtr = *rhs.compPtr; // 将右值的compPtr指向的对象复制到当前对象的compPtr指向的对象
return *this; // 返回指向当前对象的指针
}
void print() {
cout << "comp的val值为:" << compPtr->getVal() << endl;
cout << "compPtr的值为:" << compPtr << endl;
}
private:
Component *compPtr;
};
int main() {
MyClass myclass1(1);
myclass1.print();
MyClass myclass2(2);
myclass2.print();
cout << "进行赋值!" << endl;
myclass2 = myclass1;
myclass2.print();
return 0;
}
--------------------------------------------------
输出:
comp的val值为:1
compPtr的值为:0x24331f210c0
comp的val值为:2
compPtr的值为:0x24331f210e0
进行赋值!
comp的val值为:1
compPtr的值为:0x24331f210e0
在上述例子中,赋值前后,myclass2.compPtr 和 myclass1.compPtr 依旧指向不同的地址(不同的 Component 类),只是将后者指向的 Component 对象赋值给了前者,而 Component 对象没有指针,所以只是将 val 的值进行了浅拷贝。
上面例子中,在赋值之前,myclass2 就已经声明。但是如果用赋值进行初始化 myclass2,那么会调用 Copy Constructor 而不是 Overload the assignment operator:
// 赋值操作符表示的初始化
class Component {
public:
Component(int v) : val(v) {}
int getVal() { return val; }
private:
int val;
};
class MyClass {
public:
MyClass(Component cp) : comp(cp) {}
MyClass(const MyClass &myclass) : comp(myclass.comp) {
cout << "复制构造函数" << endl;
}
MyClass& operator=(const MyClass &rhs) {
cout << "赋值操作符" << endl;
comp = rhs.comp;
return *this;
}
void print() {
cout << "comp的val值为:" << comp.getVal() << endl;
}
private:
Component comp;
};
int main() {
Component comp(1);
MyClass myclass1(comp);
myclass1.print(); // -> comp的val值为:1
cout << "进行赋值!" << endl; // -> 进行赋值!
MyClass myclass2 = myclass1; // -> 复制构造函数
myclass2.print(); // -> comp的val值为:1
return 0;
}
1.4 禁止复制
如果不想让类对象在函数中按值传递导致复制,可以将 Copy Constructor 设置为 private 成员:
class Component {
public:
Component(int v) : val(v) {}
int getVal() { return val; }
private:
int val;
};
class MyClass {
public:
MyClass(Component cp) : comp(cp) {}
private:
MyClass(const MyClass &myclass) : comp(myclass.comp) { // 复制构造函数被私有化
cout << "复制构造函数" << endl;
}
Component comp;
};
int main() {
Component comp(1);
MyClass myclass1(comp);
MyClass myclass2(myclass1); // "MyClass::MyClass(const MyClass &myclass)" is inaccessible
return 0;
}
2 Virtual Function && Polymorphism (虚函数和多态)
所谓多态,就是为不同的数据类型的实体提供统一的接口,或者说不同的类可以共享一个函数,但各自的实现不同。继承是其中的一种方式,但是基类和派生类的同名函数会有函数隐藏的问题,为了实现多态,需要使用虚函数。
2.1 Virtual Function (虚函数)
举一个具体的例子:
#include <iostream>
using namespace std;
class Vehicle{
public:
Vehicle() {}
virtual void move() { cout << "交通工具行驶" << endl; } // 虚函数
};
class Airplane : public Vehicle{
public:
Airplane() {}
virtual void move() { cout << "飞机飞行" << endl; }
};
class Car : public Vehicle{
public:
Car() {}
//virtual关键词在派生类中可省略
/*virtual*/ void move() { cout << "汽车行驶" << endl; }
};
int main() {
Vehicle *vehicle = new Airplane(); // 将Airplane类初始化给基类指针vehicle
vehicle->move(); // 因为派生类的move()是虚函数,可以被基类指针调用(基类指针vehicle指向的实际是派生类Airplane的内存空间)
delete vehicle;
vehicle = new Car();
vehicle->move();
delete vehicle;
return 0;
}
---------------------------------------------------
输出:
飞机飞行
汽车行驶
我们可以用基类指针访问派生类的虚函数,这样,就可以根据用户的输入给基类指针绑定不同的派生类,以实现不同的功能,这称为 Dynamic Binding(动态绑定)。
此外,派生类的关键字 virtual 可以省略,因为编译器会自动将与基类的函数签名相同的派生类成员函数识别为虚函数。基类的关键字 virtual 不可省略(否则输出“交通工具行驶”,因为产生隐式类型转换,只有父类和虚函数被截断保留),为了程序易读性,建议基类和派生类虚函数都加上关键字 virtual。
以及,使用虚函数也需要基类的指针或引用调用,而不是基类自身:
#include <iostream>
using namespace std;
// 不用指针的情况
class Vehicle{
public:
Vehicle() {}
virtual void move() { cout << "交通工具行驶" << endl; }
};
class Airplane : public Vehicle{
public:
Airplane() {}
void move() { cout << "飞机飞行" << endl; }
};
class Car : public Vehicle{
public:
Car() {}
void move() { cout << "汽车行驶" << endl; }
};
int main() {
Airplane airplane;
Vehicle vehicle = (Vehicle)airplane;
vehicle.move(); // 不使用指针
Car car;
vehicle = (Vehicle)car;
vehicle.move();
return 0;
}
---------------------------------------------------
输出:
交通工具行驶
交通工具行驶
2.2 函数隐藏
如上述,基类的虚函数是隐藏的,要访问被隐藏的函数,需要使用作用域操作符 ::,即 派生类实例.基类名::基类虚函数 或者 派生类实例的指针->基类名::基类虚函数:
#include <iostream>
using namespace std;
// 调用被隐藏的基类函数
class Vehicle{
public:
Vehicle() {}
virtual void move() { cout << "交通工具行驶" << endl; } // 虚函数
void printName() { cout << "交通工具" << endl; }
};
class Airplane : public Vehicle{
public:
Airplane() {}
void move() { cout << "飞机飞行" << endl; }
void printName() { cout << "飞机" << endl; }
};
class Car : public Vehicle{
public:
Car() {}
void move() { cout << "汽车行驶" << endl; }
void printName() { cout << "汽车" << endl; }
};
int main() {
Airplane airplane;
airplane.Vehicle::printName(); // -> 交通工具
Vehicle *vehiclePtr = &airplane;
vehiclePtr->Vehicle::move(); // -> 交通工具行驶
Car car;
car.Vehicle::printName(); // -> 交通工具
vehiclePtr = &car;
vehiclePtr->Vehicle::move(); // -> 交通工具行驶
return 0;
}
2.3 纯虚函数
如果基类不需要虚函数,或者只有派生类需要虚函数时,可以使用纯虚函数,也就是在基类中声明该虚函数 =0:
class Vehicle{
public:
Vehicle() {}
virtual void move() = 0; // 纯虚函数
};
class Airplane : public Vehicle{
public:
Airplane() {}
void move() { cout << "飞机飞行" << endl; }
};
class Car : public Vehicle{
public:
Car() {}
void move() { cout << "汽车行驶" << endl; }
};
int main() {
Airplane airplane;
Vehicle *vehiclePtr = &airplane; // -> 飞机飞行
vehiclePtr->move();
Vehicle vehicle; // error: object of abstract class type "Vehicle" is not allowed: function "Vehicle::move" is a pure virtual function
vehiclePtr = &vehicle;
vehiclePtr->move();
return 0;
}
在基类的虚函数签名后面加上 = 0,就将其声明为了纯虚函数。这样就跳过了函数实现,同时将该类声明为 Abstract Class (抽象类),抽象类不是完整实现的,所以不能实例化,也就是不能创建抽象类的实例,不过可以创建它的指针。
2.4 虚析构函数
如果是非虚析构函数,那么在释放指向基类的指针时,会调用基类的析构函数,而不是派生类的析构函数:
// 派生类的非虚析构函数
class Vehicle{
public:
Vehicle() {}
~Vehicle() { cout << "Vehicle的析构函数被调用!" << endl; } // 非虚析构函数
virtual void move() = 0;
};
class Airplane : public Vehicle{
public:
Airplane() {}
~Airplane() { cout << "Airplane的析构函数被调用!" << endl; } // 非虚析构函数
void move() { cout << "飞机飞行" << endl; }
};
class Car : public Vehicle{
public:
Car() {}
~Car() { cout << "Car的析构函数被调用!" << endl; }
void move() { cout << "汽车行驶" << endl; }
};
int main() {
Vehicle *vehiclePtr = new Airplane();
vehiclePtr->move(); // -> 飞机飞行
delete vehiclePtr; // -> Vehicle的析构函数被调用!
vehiclePtr = new Car();
vehiclePtr->move(); // -> 汽车行驶
delete vehiclePtr; // -> Vehicle的析构函数被调用!
return 0;
}
因为虚析构函数和虚函数都放在储存基类的内存空间的开头,而一般的析构函数会像成员函数那样,在类型转换时被截取。只需要在基类的析构函数名之前加上关键字 virtual,就可以创建虚析构函数,让派生类的虚构函数得以调用:
// 虚析构函数
class Vehicle{
public:
Vehicle() {}
virtual ~Vehicle() { cout << "Vehicle的析构函数被调用!" << endl; }
virtual void move() = 0;
};
class Airplane : public Vehicle{
public:
Airplane() {}
~Airplane() { cout << "Airplane的析构函数被调用!" << endl; }
void move() { cout << "飞机飞行" << endl; }
};
class Car : public Vehicle{
public:
Car() {}
~Car() { cout << "Car的析构函数被调用!" << endl; }
void move() { cout << "汽车行驶" << endl; }
};
int main() {
Vehicle *vehiclePtr = new Airplane();
vehiclePtr->move(); // -> 飞机飞行
delete vehiclePtr; // -> Airplane的析构函数被调用! \n Vehicle的析构函数被调用!
vehiclePtr = new Car();
vehiclePtr->move(); // -> 汽车行驶
delete vehiclePtr; // -> Car的析构函数被调用! \n Vehicle的析构函数被调用!
return 0;
}
2.5 dynamic_cast
在 2. C++面向对象编程入门 # 派生类和基类的转换 提到过将派生类直接转换为基类的截断问题,而将派生类指针转换为基类指针就不会截断。而要想将指向派生类的基类指针转换回派生类的指针,同时还能访问派生类独有的成员,则可以使用 dynamic_cast:
// dynamic_cast
class Vehicle{
public:
Vehicle() {}
virtual void move() { cout << "交通工具行驶" << endl; }
};
class Airplane : public Vehicle{
public:
Airplane() {}
virtual void move() { cout << "飞机飞行" << endl; } // 虚函数
void rise() { cout << "飞机上升" << endl; } // 一般成员函数
};
class Car : public Vehicle{
public:
Car() {}
void move() { cout << "汽车行驶" << endl; } // 一般成员函数
};
int main() {
Vehicle *vehicle = new Airplane();
vehicle->move(); // -> 飞机飞行
// dynamic_cast需要在尖括号中使用指针或引用类型
Airplane *plane = dynamic_cast<Airplane *>(vehicle);
// 判断转换是否成功
if ( plane ) {
plane->rise(); // -> 飞机上升
}
delete vehicle;
vehicle = new Car();
vehicle->move(); // -> 汽车行驶
delete vehicle;
return 0;
}
2.6 * 虚函数的实现
我们知道,一般的成员函数和普通的函数一样,都是一段代码,而编译器在编译函数调用的时候会将对象的this指针作为函数隐藏的第一个参数,这样成员函数就能访问其他类成员了。对于这种非虚函数的情况来说,基类指针就算指向派生类,调用的函数版本也是基类的,因为这两个函数是不同的函数,函数入口也不同,编译器并不知道基类指针指向的是派生类。
因此,为了实现多态(动态绑定),我们需要一个在连编译器都不知道基类指针指向什么类的情况下也能调用正确虚函数的方法。而那就是,我们可以给每个类一个专属的空间,其中放置着几个自己的虚函数,指针可以通过编号来调用虚函数。虽然基类对象和派生类对象的专属空间不一样,但是虚函数的编号一样,这样编译器就算没有任何信息也能调用到正确的虚函数。这样的一个专属的存放虚函数地址的空间就是虚表(Virtual Table,也就是vtable),而由于每个类只需要一个虚表,为了节省空间,类的对象只需要一个指向虚表的虚指针(Virtual Pointer,也就是vptr)就行了。
虚表的特性:
- 每个类只有一个 vtable(静态存在,所有对象共享),节省内存;
- 当类声明了虚函数(或继承了虚函数),编译器会为该类生成一个唯一的 vtable。 vtable 是一个静态数组,存储该类所有虚函数的地址。
- 若派生类重写(关键字
override)了基类的虚函数,vtable 中对应位置会替换为派生类的函数地址; - 若派生类未重写,则沿用基类 vtable 中的函数地址;
- 若派生类有新增的虚函数,会追加到 vtable 的末尾。
- 若派生类重写(关键字
- 当类声明了虚函数(或继承了虚函数),编译器会为该类生成一个唯一的 vtable。 vtable 是一个静态数组,存储该类所有虚函数的地址。
- 派生类 vtable 会继承基类 vtable 的结构,仅替换重写的函数地址;
- 包含纯虚函数(如
virtual void func() = 0)的类(称为抽象类),其 vtable 中对应位置为nullptr或特殊标记,因此无法实例化对象; - 若基类析构函数声明为
virtual,其地址会存入 vtable,确保通过基类指针删除派生类对象时,能正确调用派生类的析构函数(避免内存泄漏)。
在 C++/2.C++面向对象编程入门#类的大小 中提到:
实际上,C++ 中,一个类的对象大小主要由以下因素决定:
- 非静态成员变量的总大小(静态成员变量不占对象内存,属于类本身);
- 因内存对齐(padding)产生的额外字节;
- 若类包含虚函数(或继承了虚函数),则对象会额外包含一个vptr(虚函数表指针),用于指向类的 vtable(虚函数表)。 在64 位系统中,指针的大小是 8 字节(32 位系统中通常是 4 字节):
class Vehicle{
public:
Vehicle() {}
virtual void move() { cout << "交通工具行驶" << endl; }
virtual void printName() { cout << "交通工具" << endl; }
};
class Airplane : public Vehicle{
public:
Airplane() {}
void move() override { cout << "飞机飞行" << endl; }
void printName() { cout << "飞机" << endl; }
};
class Car : public Vehicle{
public:
Car() {}
void move() { cout << "汽车行驶" << endl; }
void printName() { cout << "汽车" << endl; }
};
int main() {
Vehicle vehicle;
cout << "Vehicle类的大小为:" << sizeof(vehicle) << endl; // -> 8
Airplane airplane;
cout << "Airplane类的大小为:" << sizeof(airplane) << endl; // -> 8
Car car;
cout << "Car类的大小为:" << sizeof(car) << endl; // -> 8
return 0;
}
3 Overloading (操作符重载)
在 #1.3 Overload assignment (重载赋值操作符) 已经介绍过一部分有关操作符重载的内容。
3.1 一般语法
几个注意点:
- 形参的数目和操作符操作数的数目相同;
- 只适用于 1. C++基础#3.2 operators(操作符) 所介绍的现有合法操作符;
- 必须有一个
class类型的操作数,因为内置类的行为是系统已经定好的,不能也不适合修改; - 无法修改操作符的优先级和结合性;
- 避免重载逻辑操作符(
&&,||)和,,否则这些操作符不再有短路求值的特性(比如false && false不会直接短路返回false而是将&&进行一遍再返回)。
#include <iostream>
#include <string>
#include <vector>
using namespace std;
// 套餐
class Combo{
public:
Combo() {}
void add(string item) {
items.push_back(item);
}
// Combo在重载操作符实现中是const的,在其中调用的成员函数也需要声明为const
vector<string> getItems() const {
return items;
}
void printCombo() { // 打印套餐内容
cout << "套餐的内容有:" << endl;
for ( int i = 0; i < items.size(); i++ ) {
cout << items[i] << endl;
}
}
// 操作符重载的类成员形式
Combo operator+=(const Combo &cb2) { // cb2是右操作数,重载+=
// 实现:将cb2中的所有元素添加到当前对象的items中
vector<string> cb2Items = cb2.getItems();
for ( int i = 0; i < cb2Items.size(); i++ ) {
this->add(cb2Items[i]);
}
return *this;
}
private:
vector<string> items; // 套餐内容
};
// 操作符重载的非成员形式
Combo operator+(const Combo &cb1, const Combo &cb2) { // 重载+
Combo res = cb1;
vector<string> cb2Items = cb2.getItems();
for ( int i = 0; i < cb2Items.size(); i++ ) {
res.add(cb2Items[i]);
}
return res;
}
int main() {
Combo combo1;
combo1.add("汉堡");
combo1.add("薯条");
Combo combo2;
combo2.add("热狗");
combo2.add("可乐");
Combo combo3 = combo1 + combo2;
combo3.printCombo(); // -> 套餐的内容有:汉堡\n薯条\n热狗\n可乐
Combo combo4;
combo4.add("鸡翅");
combo4.add("鸡块");
combo3 += combo4;
combo3.printCombo(); // -> 套餐的内容有:汉堡\n薯条\n热狗\n可乐\n鸡翅\n鸡块
return 0;
}
在设计类的时候,对于操作符重载我们需要遵循以下的设计原则:
1.最好不要重载逗号、取址、逻辑或和逻辑与等具有系统内置意义的操作符。
2.在重载了算术操作符或位操作符之后,最好也要配套地重载相应的复合赋值操作符。
3.由于操作符重载的特殊性,调试起来并没有函数那么容易,因此能用函数代替的行为最好
还是用函数。
3.2 算数操作符重载
// 算术操作符
class Vector2D{ // 二维向量
public:
Vector2D(int X, int Y) : x(X), y(Y) {}
int x;
int y;
};
Vector2D operator+(const Vector2D &lhs, const Vector2D &rhs) { // 向量加法
Vector2D res(lhs.x + rhs.x, lhs.y + rhs.y);
return res;
}
Vector2D operator-(const Vector2D &lhs, const Vector2D &rhs) { // 向量减法
Vector2D res(lhs.x - rhs.x, lhs.y - rhs.y);
return res;
}
// 数乘向量
Vector2D operator*(const Vector2D &lhs, int rhs) { // 右乘int
Vector2D res(lhs.x * rhs, lhs.y * rhs);
return res;
}
Vector2D operator*(int lhs, const Vector2D &rhs) { // 左乘int
Vector2D res(lhs * rhs.x, lhs * rhs.y);
return res;
}
int main() {
Vector2D v1(1, 2);
Vector2D v2(3, 4);
Vector2D v3(1, 1);
int b = 2;
Vector2D v4 = 1 * ( v1 + v2 ) * 2 - v3;
cout << "v4.x:" << v4.x << "\nv4.y:" << v4.y << endl;
return 0;
}
3.3 关系操作符重载
#include <iostream>
#include <vector>
#include <algorithm> // sort排序函数需要包含此头文件
using namespace std;
// 关系操作符
class Interval{ // 区间
public:
Interval(int s, int e) : start(s), end(e) {}
int start;
int end;
};
bool operator<(const Interval &lhs, const Interval &rhs) {
if ( lhs.start < rhs.start ) {
return true;
} else if ( lhs.start == rhs.start ) {
return (lhs.end < rhs.end);
} else {
return false;
}
}
int main() {
vector<Interval> intervals;
intervals.push_back(Interval(4, 6));
intervals.push_back(Interval(1, 3));
intervals.push_back(Interval(1, 2));
intervals.push_back(Interval(2, 3));
// sort 函数在默认情况下,会使用 < 操作符来比较两个元素,且是升序排序
// intervals.begin() 返回一个迭代器(iterator),这个迭代器指向向量 intervals 中的第一个元素。
// intervals.end() 返回一个迭代器(iterator),这个迭代器指向向量 intervals 中的最后一个元素的下一个位置。
sort(intervals.begin(), intervals.end());
for ( int i = 0; i < intervals.size(); i++ ) { // 查看区间是否升序排序
cout << "x:" << intervals[i].start << " y:" << intervals[i].end << endl;
}
return 0;
}
-------------------------------------------------------
输出:
x:1 y:2
x:1 y:3
x:2 y:3
x:4 y:6
3.4 类型转换操作符重载
之前讲过 (int)floatNum 这种形式的强制转换,其中的 int 也算是一种操作符,叫作类型转换操作符,也可以重载,并且在显示和隐式转换是通用的:
// 重载转换操作符
class Complex{ // 复数类型,C++ 标准库中提供 std::complex,在 <complex> 头文件中定义
public:
Complex( ): real(0), imag(0) {} // 默认构造函数
Complex(double r,double i): real(r), imag(i) {} // 构造函数
operator double( ) const { // 转换操作符重载,将 Complex 类型转换为 double 类型
return real;
}
private:
double real;
double imag;
};
int main( ){
Complex c1(3, -4); // 复数
double doubleNum = 2.1;
// 也会被用在隐式转换
double res = doubleNum + c1;
cout << "res的值为:" << res << endl;
return 0;
}
3.5 * 自增自减操作符重载
详见:!零基础C++从入门到精通【文字版】 (零壹快学) (Z-Library)
这种冷门的功能似乎没什么用。
4 Friend (友元)
使用友元可以使某个类或函数具有对另一个类的 private 成员的访问权。
4.1 友元类
如果需要友元,可能是类的设计出现了问题,可以考虑将声明为友元的外部类作为类的成员变量,形成包含关系,并在初始化或其他函数中将该类对象传入。
在 A 类中用关键字 friend 可以声明 B 类为其友元,使得 B 类可访问 A 类的 private 成员:
#include <iostream>
#include <string>
using namespace std;
// 友元类
class Person{ // 人
friend class Detective; // 友元,不加class是没有用的
public:
Person(string n, int a, string s) : name(n), age(a), secret(s) {}
private:
string name; // 名字
int age; // 年龄
string secret; // 秘密
};
class Detective : public Person{ // 侦探,继承自 Person 类
public:
Detective(string n, int a, string s) : Person(n, a, s) {}
void investigate(const Person &p) {
cout << "调查嫌疑人:" << endl;
cout << "姓名:" << p.name << endl;
cout << "年龄:" << p.age << endl;
cout << "嫌疑人的秘密是:" << p.secret << endl;
}
};
int main() {
Detective cogi("侦探柯基", 7, "有一副智能眼镜");
Detective saburo("侦探三郎", 38, "喜欢喝酒");
cogi.investigate(saburo);
return 0;
}
------------------------------------------------------
输出:
调查嫌疑人:
姓名:侦探三郎
年龄:38
嫌疑人的秘密是:喜欢喝酒
4.2 友元函数
与友元类类似,单个函数也可以在一个类中被声明为友元,使得可以在函数中访问类的成员。用关键字 friend 声明友元函数时,需要完整的函数签名。其他类的成员函数也可以作为友元函数(较为复杂)。
#include <iostream>
#include <string>
using namespace std;
class Person{ // 人
friend void printName(const Person &p); // 友元函数,用于打印 Person 类的姓名
public:
Person(string n, int a, string s) : name(n), age(a), secret(s) {}
private:
string name; // 名字
int age; // 年龄
string secret; // 秘密
};
void printName(const Person &p) { // 友元函数
cout << "姓名:" << p.name << endl;
}
int main() {
Person xiaoming("小明", 10, "不喜欢吃蔬菜");
printName(xiaoming);
return 0;
}
----------------------------------------------------
输出:
姓名:小明
4.3 友元和继承
按照C++标准中的规定,友元关系并不能继承,所以类继承后,友元关系需要重新声明:
#include <iostream>
#include <string>
using namespace std;
class Person{
friend class Detective; // 不加class是没有用的
public:
Person(string n, int a, string s) : name(n), age(a), secret(s) {}
protected:
string name; // 名字
int age; // 年龄
string secret; // 秘密
};
class Student : public Person{
friend class Detective; // 为了访问派生类特有成员,注释掉后不能访问school
public:
Student(string n, int a, string s, string sch) : Person(n, a, s), school(sch) {}
protected:
string school; // 学校
};
class Detective : public Person{
public:
Detective(string n, int a, string s) : Person(n, a, s) {}
void investigate(const Person &p) {
cout << "调查嫌疑人:" << endl;
cout << "姓名:" << p.name << endl;
cout << "年龄:" << p.age << endl;
cout << "嫌疑人的秘密是:" << p.secret << endl;
}
void investigateStudent(const Student &stu) {
cout << "调查学生:" << endl;
cout << "姓名:" << stu.name << endl;
cout << "年龄:" << stu.age << endl;
cout << "学校:" << stu.school << endl;
}
};
int main() {
Detective cogi("侦探柯基", 7, "有一副智能眼镜");
Detective mouri("侦探三郎", 38, "喜欢喝酒");
Student fujiwara("藤原爱", 7, "喜欢侦探柯基", "堤旦小学");
cogi.investigate(mouri);
cogi.investigateStudent(fujiwara);
return 0;
}
--------------------------------------------------
输出:
调查嫌疑人:
姓名:侦探三郎
年龄:38
嫌疑人的秘密是:喜欢喝酒
调查学生:
姓名:藤原爱
年龄:7
学校:堤旦小学
5 * private Constructor (私有构造函数)
在一些情况下,我们可能希望创建特别的构造对象的函数,而又不希望外界依然能使用原始的构造函数,这个时候我们可以把构造函数修饰为 private。比如一个类有好多个版本的构造函数,但我们不希望用户自己选择版本,而是为他们提供一些选项,好让我们在构建的函数中智能地帮助用户选择构造函数。
class MyClass{
public:
static MyClass* buildMyClass(int v) { return (new MyClass(v)); }
static void destroyMyClass(MyClass *ptr) { delete ptr; }
void printVal() const { cout << "val的值为:" << val << endl; }
private:
MyClass(int v): val(v) {} // 私有构造函数
MyClass(const MyClass &myclass): val(myclass.val) {} // 私有复制构造函数
~MyClass() {} // 私有析构函数
int val;
};
void printMyClass( const MyClass &myclass) {
myclass.printVal();
}
// 复制构造函数为私有,不能再按值传参
/*void printMyClass( MyClass myclass) {
myclass.printVal();
}*/
int main(){
MyClass *p = MyClass::buildMyClass(2);
printMyClass(*p); // -> val的值为:2
MyClass::destroyMyClass(p);
return 0;
}